package lang4j.parser;

import org.apache.commons.beanutils.BeanUtils;

import java.util.*;
import java.lang.reflect.InvocationTargetException;

/**
 * This thing is needed for anything that makes up a scope for IDs.
 */
public class IdentifierMap {

    public static class ProductionEntry {


        class PropertyPointer implements ReferencePointer {
            Object object;
            String field;

            public String toString() {
                return  field+"->'"+object+"'";
            }

            public PropertyPointer(final String field, final Object object) {
                this.field = field;
                this.object = object;
            }

            public String getField() {
                return field;
            }

            public Object getObject() {
                return object;
            }


            public void setValue(final Object value) {
                try {
                    BeanUtils.setProperty(object, field, value);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                } catch (InvocationTargetException e) {
                    throw new RuntimeException(e);
                }
            }
        }

        public static class ListElementPointer implements ReferencePointer {
            List list;
            int index;


            public ListElementPointer(final List list, final int index) {
                this.index = index;
                this.list = list;
            }

            public String toString() {
                return "listRef:"+index;
            }

            public void setValue(final Object value) {
                list.set(index, value);
            }
        }

        public static interface ReferencePointer {
            public void setValue(Object value);
        }

        public void addInstance(final String key, final Object value) {
            if (idToInstanceMap.containsKey(key)) throw new RuntimeException("Duplicate Identifier "+key+" for "+productionName);
            idToInstanceMap.put(key, value);

            if (unresolvedIds.containsKey(key)) {
                List unresolved=(List)( unresolvedIds.get(key));
                for (Iterator iterator = unresolved.iterator(); iterator.hasNext();) {
                    ReferencePointer pointer = (ReferencePointer) ((TaggedValue)iterator.next()).getValue();
                    pointer.setValue(value);
                }
                unresolvedIds.remove(key);
            }

        }

        public void resolveReference(String key,int index, List list,String tag){
            ListElementPointer pointer = new ListElementPointer(list, index);
            resolveReference(key,pointer,tag);

        }

        public void resolveReference(final String key, final String foreignKeyName, final Object instance,String tag) {
            final PropertyPointer pointer = new PropertyPointer(foreignKeyName, instance);
            resolveReference(key, pointer,tag);
        }

        private void resolveReference(final String key, final ReferencePointer pointer,String tag) {
            Object lookup = idToInstanceMap.get(key);
            if (lookup != null) {
                pointer.setValue(lookup);
            } else {
                enqueForResolution(key, pointer,tag);
            }
        }

        private void enqueForResolution(final String key, final ReferencePointer pointer,String tag) {
            if (unresolvedIds.get(key)==null){
                unresolvedIds.put(key,new LinkedList());
            }
            ((List)unresolvedIds.get(key)).add(new TaggedValue(tag,pointer));
        }

        public boolean isResolved() {
            return unresolvedIds.size() == 0;
        }



        private void setValue(final Object bean, final String propertyName, final Object value) {
            try {
                BeanUtils.setProperty(bean, propertyName, value);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            } catch (InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        }


        public ProductionEntry(final String productionName) {
            this.productionName = productionName;
            idToInstanceMap = new HashMap();
            unresolvedIds = new HashMap();
        }


        private String productionName; //somewhat redundant
        private Map idToInstanceMap; //resolved instances
        private Map unresolvedIds; //IDs to instances (and prpoerty names?

        public Map getIdToInstanceMap() {
            return Collections.unmodifiableMap(idToInstanceMap);
        }

        public Map getUnresolvedIds() {
            return Collections.unmodifiableMap(unresolvedIds);
        }
    }

    private Map productionMap; //hold instances of ProductionEntry

    public IdentifierMap() {
        productionMap = new HashMap();
    }

    private ProductionEntry getProductionEntry(String productionName) {
        ProductionEntry entry = (ProductionEntry) productionMap.get(productionName);
        if (entry == null) {
            entry = new ProductionEntry(productionName);
            productionMap.put(productionName, entry);
        }
        return entry;
    }

    public boolean isResolved() {
        for (Iterator iterator = productionMap.values().iterator(); iterator.hasNext();) {
            ProductionEntry productionEntry = (ProductionEntry) iterator.next();
            if (!productionEntry.isResolved())
                return false;
        }
        return true;
    }

    public Collection getUnresolved() {
        Collection temp=new java.util.ArrayList();
        for (Iterator iterator = productionMap.values().iterator(); iterator.hasNext();) {
            ProductionEntry productionEntry = (ProductionEntry) iterator.next();
            final Map unresolvedIds = productionEntry.getUnresolvedIds();
            for (Iterator iterator1 = unresolvedIds.keySet().iterator(); iterator1.hasNext();) {
                Object key = (Object) iterator1.next();
                temp.add(new TaggedValue(key.toString(),unresolvedIds.get(key)));
            }

        }
        return temp;

    }

    /**
     * Adds the given instance to the identiry map for the given production
     */
    public void addInstance(String production, String key, Object instance) {
        getProductionEntry(production).addInstance(key, instance);


    }

    /**
     * Adds the given unresolved reference to the map of unresolved references
     */
    public void resolveReference(String production, String key, String foreignKeyName, Object instance) {
        getProductionEntry(production).resolveReference(key, foreignKeyName, instance,"No info");
    }

    public void resolveReference(String production, String key, List list, int index){
         getProductionEntry(production).resolveReference(key,index,list,"No info");

    }

    public void resolveReference(String production, String key, String foreignKeyName, Object instance,String tag) {
        getProductionEntry(production).resolveReference(key, foreignKeyName, instance,tag);
    }

    public void resolveReference(String production, String key, List list, int index,String tag){
         getProductionEntry(production).resolveReference(key,index,list,tag);

    }


    public Map getProductionMap() {
        return Collections.unmodifiableMap(productionMap);
    }


    public static class TaggedValue {
        String tag;
        Object value;

        public TaggedValue(final String tag, final Object value) {
            this.tag = tag;
            this.value = value;
        }

        public String getTag() {
            return tag;
        }

        public Object getValue() {
            return value;
        }

        public String toString() {
            return tag+": "+value.toString();
        }
    }
}
